﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reflection;
using MugenInjection.Activators;
using MugenInjection.Attributes;
using MugenInjection.Delegates;
using MugenInjection.Infrastructure;
using MugenInjection.Interface;
using MugenInjection.Interface.Components;

// ReSharper disable CheckNamespace
namespace MugenInjection.Core.Components
// ReSharper restore CheckNamespace
{
    /// <summary>
    ///     Represents the base component that activates bindings.
    /// </summary>
    public class BindingActivatorComponent : Component, IBindingActivatorComponent
    {
        #region Nested type: ServiceTypeCache

        private sealed class ServiceTypeCache
        {
            public readonly IConverter Converter;
            public readonly Func<object, object> ConverterAction;
            public readonly Type OriginalType;
            public readonly ServiceType ServiceType;

            /// <summary>
            /// Initializes a new instance of the <see cref="ServiceTypeCache"/> class.
            /// </summary>
            public ServiceTypeCache(IConverter converter, Func<object, object> converterAction, Type originalType, ServiceType serviceType)
            {
                ConverterAction = converterAction;
                OriginalType = originalType;
                ServiceType = serviceType;
                Converter = converter;
            }
        }

        #endregion

        #region Fields

        private readonly object _activatorLocker = new object();
        private readonly List<IConverter> _converters;
        private readonly CacheCollection<Type, ServiceTypeCache> _serviceTypeCaches;
        private IActivator _activatorToInject;

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="BindingActivatorComponent"/> class.
        /// </summary>
        public BindingActivatorComponent()
            : this(null)
        {
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="BindingActivatorComponent" /> class.
        /// </summary>
        public BindingActivatorComponent(IEnumerable<IConverter> converters)
        {
            _serviceTypeCaches = new CacheCollection<Type, ServiceTypeCache>();
            _converters = new List<IConverter>();
            if (converters != null)
            {
                foreach (var converter in converters)
                {
                    // ReSharper disable DoNotCallOverridableMethodsInConstructor
                    AddConverter(converter);
                    // ReSharper restore DoNotCallOverridableMethodsInConstructor
                }
            }
            else
            {
                // ReSharper disable DoNotCallOverridableMethodsInConstructor
                AddConverter(new SimpleConverter());
                AddConverter(new CollectionConverter());
                AddConverter(new FactoryAndLazyConverter());
                AddConverter(new InjectorWrapperConverter());
                AddConverter(new SettingsWrapperConverter());
                AddConverter(new BindingSettingsConverter());
                // ReSharper restore DoNotCallOverridableMethodsInConstructor
            }
            Converters = new ReadOnlyCollection<IConverter>(_converters);
        }

        #endregion

        #region Overrides of Component

        /// <summary>
        ///     Indicates whether the component is unique.
        /// </summary>
        public override sealed bool IsSingle
        {
            get { return true; }
        }

        /// <summary>
        ///     Gets the component type. Need to find a component in the container.
        /// </summary>
        public override Type ComponentType
        {
            get { return typeof(IBindingActivatorComponent); }
        }

        /// <summary>
        /// Occurs on load component.
        /// </summary>
        protected override void OnLoad()
        {
            ActivatorToInject = Injector.Settings.DefaultActivatorFactory();
            base.OnLoad();
        }

        /// <summary>
        /// Occurs on unload component.
        /// </summary>
        protected override void OnUnload()
        {
            lock (_activatorLocker)
            {
                if (_activatorToInject != null)
                {
                    _activatorToInject.Dispose();
                    _activatorToInject = null;
                }
            }
            base.OnUnload();
        }

        /// <summary>
        ///     Creates a new object that is a copy of the current instance.
        /// </summary>
        /// <returns>
        ///     A new object that is a copy of this instance.
        /// </returns>
        public override IComponent Clone()
        {
            lock (_serviceTypeCaches)
                return new BindingActivatorComponent(_converters);
        }

        #endregion

        #region Methods

        private void ClearConvertersCache()
        {
            foreach (var converter in _converters)
                converter.ClearCache();
        }

        private static void ValidateConverter(IConverter converter)
        {
            bool isSimple = InjectorUtilsInternal.HasFlag(converter.ServiceType, ServiceType.Simple);
            bool isComplex = InjectorUtilsInternal.HasFlag(converter.ServiceType, ServiceType.Complex);
            if (isSimple && isComplex)
                throw new ArgumentException("The converter can only be simple or complex type.", "converter");
            if (!isSimple && !isComplex)
                throw new ArgumentException("The converter can only be simple or complex type.", "converter");
        }

        /// <summary>
        ///     Occurs when the <c>SupportedTypes</c> changed.
        /// </summary>
        protected virtual void ConverterOnSupportedTypeChanged(IConverter sender, TypeChangedAction action, Type type)
        {
            lock (_serviceTypeCaches)
            {
                var listToDelete = new List<Type>();
                foreach (var serviceTypeCach in _serviceTypeCaches.Cache)
                {
                    if (serviceTypeCach.Key != type && !InjectorUtilsInternal.IsEqualGenericDefinition(serviceTypeCach.Key, type)) continue;
                    listToDelete.Add(serviceTypeCach.Key);
                }
                for (int index = 0; index < listToDelete.Count; index++)
                    _serviceTypeCaches.Remove(listToDelete[index]);
            }
        }

        #endregion

        #region Implementation of IBindingActivatorComponent

        /// <summary>
        ///     Gets or sets a <see cref="IActivator" /> to inject values into properties, fields, methods in the specified object.
        /// </summary>
        public IActivator ActivatorToInject
        {
            get
            {
                Validate.EnsureIsInitializedValue(_activatorToInject, "ActivatorToInject");
                lock (_activatorLocker)
                    return _activatorToInject;
            }
            set
            {
                Validate.PropertyNotNull(value, "ActivatorToInject");
                lock (_activatorLocker)
                    _activatorToInject = value;
            }
        }

        /// <summary>
        ///     Gets the converters.
        /// </summary>
        public IList<IConverter> Converters { get; private set; }

        /// <summary>
        ///     Adds the specified <see cref="IConverter" />.
        /// </summary>
        /// <param name="converter">
        ///     The specified <see cref="IConverter" />.
        /// </param>
        public virtual void AddConverter(IConverter converter)
        {
            Validate.ArgumentNotNull(converter, "converter");
            lock (_serviceTypeCaches)
            {
                if (_converters.Contains(converter))
                    return;
                ValidateConverter(converter);
                _converters.Add(converter);
                _converters.Sort((converter1, converter2) => converter2.Priority.CompareTo(converter1.Priority));
                _serviceTypeCaches.Clear();
                ClearConvertersCache();
                converter.SupportedTypeChanged += ConverterOnSupportedTypeChanged;
            }
        }

        /// <summary>
        ///     Removes the specified <see cref="IConverter" />.
        /// </summary>
        /// <param name="converter">
        ///     The specified <see cref="IConverter" />.
        /// </param>
        public virtual bool RemoveConverter(IConverter converter)
        {
            Validate.ArgumentNotNull(converter, "converter");
            lock (_serviceTypeCaches)
            {
                if (!_converters.Contains(converter))
                    return false;

                var listToDelete = new List<Type>();
                foreach (var valuePair in _serviceTypeCaches.Cache)
                {
                    if (valuePair.Value.Converter == converter)
                        listToDelete.Add(valuePair.Key);
                }
                for (int index = 0; index < listToDelete.Count; index++)
                    _serviceTypeCaches.Remove(listToDelete[index]);
                ClearConvertersCache();
                converter.SupportedTypeChanged -= ConverterOnSupportedTypeChanged;
                return _converters.Remove(converter);
            }
        }

        /// <summary>
        /// Gets a converter use the specified type.
        /// </summary>
        /// <typeparam name="T">The specified type.</typeparam>
        /// <returns>An instance of <see cref="IConverter"/>.</returns>
        public virtual T GetConverter<T>() where T : class, IConverter
        {
            lock (_serviceTypeCaches)
            {
                foreach (var converter in Converters)
                {
                    var result = converter as T;
                    if (result != null)
                        return result;
                }
            }
            return null;
        }

        /// <summary>
        ///     Activates the specified <see cref="IBinding" /> with specified <see cref="IBindingContext" />.
        /// </summary>
        /// <param name="binding">
        ///     The specified <see cref="IBinding" />.
        /// </param>
        /// <param name="bindingContext">
        ///     The specified <see cref="IBindingContext" />.
        /// </param>
        /// <returns>An instance of service.</returns>
        public virtual object Activate(IBinding binding, IBindingContext bindingContext)
        {
            return binding.Resolve(bindingContext);
        }

        /// <summary>
        ///     Identifies the object type and create a converter for result object.
        /// </summary>
        /// <param name="context">The <see cref="IBindingContext"/> to identify.</param>
        /// <param name="originalType">The original type of service. </param>
        /// <param name="converterAction">A method to convert object(s).</param>
        /// <returns>A service type value.</returns>
        public virtual ServiceType GetServiceType(IBindingContext context, out Type originalType, out Func<object, object> converterAction)
        {
            lock (_serviceTypeCaches)
            {
                ServiceTypeCache serviceTypeCache = _serviceTypeCaches[context.Service];
                if (serviceTypeCache == null)
                {
                    foreach (IConverter converter in _converters)
                    {
                        var converterResult = converter.TryConvert(context, out originalType, out converterAction);
                        if (converterResult == ConverterResult.SupportedWithCache)
                        {
                            serviceTypeCache = new ServiceTypeCache(converter, converterAction, originalType,
                                                                    converter.ServiceType);
                            break;
                        }
                        if (converterResult == ConverterResult.SupportedWithoutCache)
                            return converter.ServiceType;
                    }
                    if (serviceTypeCache == null)
                        serviceTypeCache = new ServiceTypeCache(null, null, context.Service, ServiceType.Simple);
                    _serviceTypeCaches[context.Service] = serviceTypeCache;
                }
                originalType = serviceTypeCache.OriginalType;
                converterAction = serviceTypeCache.ConverterAction;
                return serviceTypeCache.ServiceType;
            }
        }

        /// <summary>
        ///     Injects values into properties, fields, methods in the specified object.
        /// </summary>
        /// <param name="target">The specified target for inject.</param>
        /// <param name="bindingContext">
        ///     The specified <see cref="IBindingContext" />.
        /// </param>
        public virtual void Inject<T>(ref T target, IBindingContext bindingContext)
        {
            lock (_activatorLocker)
                _activatorToInject.Activate(ref target, bindingContext);
        }

        /// <summary>
        ///     Gets parameters for the specified <see cref="MethodBase" />.
        /// </summary>
        /// <param name="methodBase">
        ///     The specified <see cref="MethodBase" />.
        /// </param>
        /// <param name="bindingContext">
        ///     The specified <see cref="IBindingContext" />.
        /// </param>
        /// <returns>An instances of services.</returns>
        public virtual object[] GetParameters(MethodBase methodBase, IBindingContext bindingContext)
        {
            ParameterInfo[] parameterInfos = InjectorUtilsInternal.GetParameterInfo(methodBase);
            if (parameterInfos.Length == 0)
                return InjectorUtilsInternal.EmptyParams;

            IList<IInjectionParameter> parameters = bindingContext.Parameters;
            ParameterContext parameterContext = null;
            int count = parameters.Count;
            if (count > 0)
                parameterContext = new ParameterContext(Injector);
            var result = new object[parameterInfos.Length];
            //Search constructor arguments
            for (int i = 0; i < parameterInfos.Length; i++)
            {
                ParameterInfo parameterInfo = parameterInfos[i];
                bool find = false;
                if (count > 0)
                {
                    for (int index = 0; index < count; index++)
                    {
                        IInjectionParameter injectionParameter = parameters[index];
                        if (!injectionParameter.CanResolve(methodBase, parameterInfo)) continue;
                        parameterContext.InjectedType = methodBase.DeclaringType;
                        result[i] = injectionParameter.GetValue(parameterContext);
                        find = true;
                        break;
                    }
                }
                if (find) continue;

                var context = new BindingContext(parameterInfo.ParameterType, methodBase, parameterInfo, methodBase.DeclaringType, bindingContext.Injector, bindingContext);
#if NETFX_CORE                
                var resolverAttribute = context.ParameterInfo.GetCustomAttribute<ResolverAttribute>(true);                
#else
                var resolverAttribute = InjectorUtilsInternal.GetAttribute<ResolverAttribute>(parameterInfo);
#endif
                if (resolverAttribute != null && resolverAttribute.CanResolve(context))
                    result[i] = resolverAttribute.Resolve(context);
                else
                    result[i] = bindingContext.Injector.Resolve(context);
            }

            return result;
        }

        /// <summary>
        ///     Gets parameter for the specified <see cref="MemberInfo" />.
        /// </summary>
        /// <param name="memberInfo">
        ///     The specified <see cref="MemberInfo" />.
        /// </param>
        /// <param name="bindingContext">
        ///     The specified <see cref="IBindingContext" />.
        /// </param>
        /// <returns>An instance of service.</returns>
        public virtual object GetParameter(MemberInfo memberInfo, IBindingContext bindingContext)
        {
            IList<IInjectionParameter> parameters = bindingContext.Parameters;
            if (parameters.Count > 0)
            {
                for (int i = 0; i < parameters.Count; i++)
                {
                    IInjectionParameter injectionParameter = parameters[i];
                    if (!injectionParameter.CanResolve(memberInfo, null)) continue;
                    return injectionParameter.GetValue(new ParameterContext(Injector, memberInfo.DeclaringType));
                }
            }
#if NETFX_CORE
            MemberTypes memberType = memberInfo.GetMemberType();
#else
            MemberTypes memberType = memberInfo.MemberType;
#endif
            Type type;
            switch (memberType)
            {
                case MemberTypes.Field:
                    type = ((FieldInfo)memberInfo).FieldType;
                    break;
                case MemberTypes.Property:
                    type = ((PropertyInfo)memberInfo).PropertyType;
                    break;
                default:
                    throw Validate.EnumOutOfRange(memberType, "memberType");
            }

            var context = new BindingContext(type, memberInfo, null, memberInfo.DeclaringType, bindingContext.Injector, bindingContext);
            var resolverAttribute = InjectorUtilsInternal.GetAttribute<ResolverAttribute>(context.Member);
            if (resolverAttribute != null && resolverAttribute.CanResolve(context))
                return resolverAttribute.Resolve(context);
            return bindingContext.Injector.Resolve(context);
        }

        #endregion
    }
}